#!/usr/bin/env python
# encoding: utf-8

"""
    模拟测试模块，针对策略搭配模拟账户
    模拟账户独立出已有账户
    输入:
        1. 起始资金: money
        2. 策略名称和策略内容
        3. 股票ID, 市场ID, 随机股票(数量) mode_code,value
        4. 模式：历史模式，模拟模式 mode_time
            4.1. 起始日期，终止日期
            4.2. 随机起始日期，时间长度
            4.3. 时间长度（数据采用正态分布随机生成）
        5. 买入/卖出　通过率
        7. Key: hash 1-5数据得到【默认】
    输出信息：
        1. 资金变化趋势
        2. 买入时机和卖出时机(在下面的数据上标记)
        3. 时间，股票，价格
        4. 数据汇总-买入次数,卖出次数,平均卖出量,平均买入量,盈利率,回测率
"""
from source.Lib.Tools import Outer
from source.StrategyCombin.main import StrategyResolve
from source.Lib import orm
from sqlalchemy.sql import func
import json
import random
import hashlib
import datetime


def main():
    """
        1. 生成X轴数据
        2. 调用计算接口，判断股票
            1. 根据策略ID获取N个formulas
            2. 计算公式结果，判断策略通过率
        3. 根据输入模式，重复上述过程
    """
    outer = Outer(channel="SimulationSignal,SimulationMatch")

    # 股票 -> 30个数值字段(1~29) -> [数值，数值] 100个，概率越大占据位置越多，使用random(1~100)选取
    # 样本数据为增长率，之后再进行计算生成第二天数据
    ps = outer.get_ps()
    for item in ps.listen():
        if item['type'] == 'message':
            input_args = json.loads(bytes.decode(item['data']))
            redis_key = input_args['redis_key']

            try:
                if 'SimulationSignal' in bytes(item['channel']).decode():
                    simulation_signal(outer, redis_key, input_args)
                else:
                    simulation_match(outer, redis_key, input_args)
            except (Exception, ) as e:
                outer.put(redis_key, json.dumps({'ret': False, 'data': "", 'msg': f'error:{e}'}))


def range_market(st, ed, codes, session):
    dates = dict()
    for _code in codes:
        try:
            _history = session.query(orm.StockDay).filter(
                orm.StockDay.date >= st
            ).filter(
                orm.StockDay.date <= ed
            ).filter(
                orm.StockDay.code == _code
            ).filter(
                orm.StockDay.open != 0
            ).all()[0]
        except (IndexError, ):
            continue

        for _row in session.query(orm.StockDay).filter(
                orm.StockDay.date >= st
        ).filter(
            orm.StockDay.date <= ed
        ).filter(
            orm.StockDay.code == _code
        ).all():
            dates.setdefault(_row.date.strftime("%Y-%m-%d"), {})
            if _row.open != 0:
                dates[_row.date.strftime("%Y-%m-%d")][_code] = ",".join(list(map(lambda x: str(x), [
                    _row.code, _row.open, _row.close,
                    _row.close, _row.high, _row.low,
                    _row.close, _row.close, _row.voturnover,
                    _row.vaturnover])))
                _history = _row
            else:
                dates[_row.date.strftime("%Y-%m-%d")][_code] = ",".join(list(map(lambda x: str(x), [
                    _history.code, _history.open, _history.close,
                    _history.close, _history.high, _history.low,
                    _history.close, _history.close, _history.voturnover,
                    _history.vaturnover])))
    return dates


def get_dates(input_args, codes, session):
    if input_args['mode_time']['key'] == 'history':
        st_date, end_date = input_args['mode_time']['value'].split(',')
        st_date = datetime.datetime.strptime(st_date, "%Y-%m-%d")
        end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d")
        dates = range_market(st_date, end_date, codes, session)
    elif input_args['mode_time']['key'] == 'random':
        min_code_date = list()
        for code in codes:
            min_code_date.append(session.query(
                func.min(orm.StockDay.date)
            ).filter(orm.StockDay.code == code).one()[0])
        st_date = max(min_code_date)
        end_date = st_date + datetime.timedelta(days=int(input_args['mode_time']['value'].split(',')[0]))

        dates = range_market(st_date, end_date, codes, session)
    else:
        # 采集生成market, 虚假的起始和终止日期,　优化：使用同行业所有数据作为样本采样。
        dates = dict()
        st_date = datetime.datetime.strptime('1111-11-11', '%Y-%m-%d')

        nd_data = {}
        for code in codes:
            nd_data[code] = create_imitate_market(
                session.query(
                    orm.StockDay
                ).filter(
                    orm.StockDay.code == code
                ).all(), int(input_args['mode_time']['value'].split(',')[0])
            )

        for i in range(0, int(input_args['mode_time']['value'].split(',')[0])):
            now = st_date + datetime.timedelta(days=i)
            dates[now.strftime("%Y-%m-%d")] = {}
            for code in codes:
                dates[now.strftime("%Y-%m-%d")][code] = nd_data[code][i]
    return dates


def get_codes(input_args, session):
    if input_args['mode_code']['key'] == 'code':
        codes = input_args['mode_code']['value'].split(',')
    elif input_args['mode_code']['key'] == 'industry':
        codes = session.query(orm.Industry).filter(orm.Industry.id == input_args['mode_code']['value']).first().codes
    else:
        codes = list(map(lambda x: x[0],
                         random.sample(
                             session.query(orm.StockInfo.code).all(),
                             int(input_args['mode_code']['value']))))
    return codes


def get_market_no_zero(dates, str_datetime, code):
    market = dates[str_datetime][code]
    price = float(market.split(',')[3])
    if price == 0:
        d = datetime.datetime.strptime(str_datetime, '%Y-%m-%d')
        while d.strftime("%Y-%m-%d") in dates and price == 0:
            market = dates[d.strftime("%Y-%m-%d")][code]
            price = float(market.split(',')[3])
            d -= datetime.timedelta(days=1)

        d += datetime.timedelta(days=2)
        while d.strftime("%Y-%m-%d") in dates and price == 0:
            market = dates[d.strftime("%Y-%m-%d")][code]
            price = float(market.split(',')[3])
            d += datetime.timedelta(days=1)
    return market, price


def calc_formulas(formulas, code, market, str_datetime):
    true, total = 0, 0
    temp_quota = dict()
    for formula in formulas:
        answer, per = formula.Calc(code=code, imitate=market, date=str_datetime)
        total += 1
        if answer():
            true += 1
        temp_quota.update(per)

    current_rate = round(true / total * 100, 2)
    return current_rate, temp_quota


def calc_profit(input_args, money, rates, price, position_use):
    buy_st, buy_ed = map(lambda x: float(x), input_args['buy_rate'].split(','))
    sell_st, sell_ed = map(lambda x: float(x), input_args['sell_rate'].split(','))
    if buy_st <= rates <= buy_ed and money/price > 100:
        position = random.randint(100, int(money/price))/10
        position = 100 if position < 100 else int(position / 100) * 100
        if position > 0:
            flag = '+'
        else:
            flag = ''

        profit = (position * price) * -1
    elif sell_st <= rates <= sell_ed and position_use > 0:
        flag = '-'
        sell_position = int(random.randint(100, int(position_use)) / 100) * 100
        profit = sell_position * price
        position = -1 * sell_position
    else:
        flag = ''
        profit = 0
        position = 0
    return flag, profit, position


def calc_all_dates(input_args, dates, formulas):
    money = int(input_args['money'])
    position_count = dict()
    # 买入次数,卖出次数,平均卖出量,平均买入量,盈利率
    result = {
        'pass': dict(), 'flag': dict(), 'market': dict(),
        'money': list(), 'date': list(), 'quota': dict(),
        'total': {'buy_count': 0, 'sell_count': 0, 'buy_position': 0, 'sell_position': 0,
                  'profit': 0, 'avg_profit_money': 0, 'avg_loss_money': 0}
    }

    profit_histroy = list()
    profit_rate = {'count': 0, 'win': 0, 'data': 0, 'flag': ''}
    for str_datetime in dates:
        position_money = 0
        for code in dates[str_datetime]:
            market, price = get_market_no_zero(dates, str_datetime, code)

            position_count.setdefault(code, 0)
            result['market'].setdefault(code, [])
            result['pass'].setdefault(code, [])
            result['quota'].setdefault(code, [])
            result['flag'].setdefault(code, [])

            rates, quotas = calc_formulas(formulas, code, market, str_datetime)

            result['quota'][code].append(quotas)
            result['pass'][code].append(rates)

            flag, profit, position = calc_profit(input_args, money, rates, price, position_count[code])
            if profit_rate['data'] != 0:
                if profit_rate['flag'] == '+' and profit_rate['data'] > price:
                    profit_rate['win'] += 1
                elif profit_rate['flag'] == '-' and profit_rate['data'] < price:
                    profit_rate['win'] += 1

                profit_rate['flag'] = ''
                profit_rate['data'] = 0

            if flag:
                profit_rate['count'] += 1
                profit_rate['data'] = price
                profit_rate['flag'] = flag
                profit_histroy.append(profit)
                if flag == '+':
                    result['total']['buy_count'] += 1
                    result['total']['buy_position'] += position
                elif flag == '-':
                    result['total']['sell_count'] += 1
                    result['total']['sell_position'] += (-1*position)

            result['flag'][code].append(flag)
            money += profit
            position_count[code] += position

            position_money += position_count[code] * price

            result['market'][code].append(market)

        result['date'].append(str_datetime)
        result['money'].append(round(money + position_money, 2))

    win = list(filter(lambda x: x > 0, profit_histroy))
    loss = list(filter(lambda x: x < 0, profit_histroy))
    result['total']['profit'] = round(profit_rate['win'] / profit_rate['count'] * 100, 1)
    if len(win) == 0:
        result['total']['avg_profit_money'] = 0
    else:
        result['total']['avg_profit_money'] = round(sum(win)/len(win), 1)
    if len(loss) == 0:
        result['total']['avg_loss_money'] = 0
    else:
        result['total']['avg_loss_money'] = round(sum(loss)/len(loss)*-1, 1)
    return result


def simulation_signal(outer, redis_key, input_args):
    s = orm.Session()

    codes = get_codes(input_args, s)
    outer.put(redis_key, json.dumps({'ret': False, 'data': {}, 'msg': '(2/4)代码生成成功'}))

    dates = get_dates(input_args, codes, s)
    outer.put(redis_key, json.dumps({'ret': False, 'data': {}, 'msg': '(3/4)行情生成成功'}))

    formulas = StrategyResolve(name=input_args['strategy'], formulas=input_args['formulas'])

    s.close()

    result = calc_all_dates(input_args, dates, formulas)
    outer.put(redis_key, json.dumps({'ret': True, 'data': result, 'msg': '(4/4)计算完成'}))


def simulation_match(outer, redis_key, input_args):
    session = orm.Session()
    max_count = int(input_args['mode_code']['value'])
    current_count = 0

    # 写死限制,避免运算量过大
    if max_count > 10:
        max_count = 10

    codes = list(map(lambda x: x[0], session.query(orm.StockInfo.code).all()))
    random.shuffle(codes)
    for code in codes:
        markets = get_dates(input_args, [code, ], session)
        if not markets:
            continue
        formulas = StrategyResolve(name=input_args['strategy'], formulas=input_args['formulas'])
        result = calc_all_dates(input_args, markets, formulas)

        all_clear = True
        up_rate = int(input_args['up'])
        down_rate = int(input_args['down'])

        for rate in result['pass'][code]:
            if rate < down_rate or rate > up_rate:
                all_clear = False
                break

        if all_clear:
            outer.put(f"{redis_key}:{current_count}",
                      json.dumps({'code': code, 'data': result}))
            current_count += 1

        if current_count == max_count:
            break

    session.close()


def normal_distribution(datas):
    rates = dict()
    for i in range(0, len(datas)-2):
        try:
            rate = round(datas[i+1]/datas[i], 4)
            if rate > 1.1:
                rate = 1.1
            elif rate < 0.9:
                rate = 0.9
        except ZeroDivisionError:
            continue
        rates.setdefault(rate, 0)
        rates[rate] += 1

    result = []
    for rate in rates:
        for i in range(0, rates[rate]):
            result.append(rate)
    return result


def create_imitate_market(stockdays, create_days):
    datas = [
        [_.open for _ in stockdays],
        [_.close for _ in stockdays],
        [_.close for _ in stockdays],
        [_.high for _ in stockdays],
        [_.low for _ in stockdays],
        [_.close for _ in stockdays],
        [_.close for _ in stockdays],
        [_.voturnover for _ in stockdays],
        [_.vaturnover for _ in stockdays],
    ]
    new_datas = []
    for col in datas:
        rates = normal_distribution(col)

        new_col = list()
        init_value = random.choice(col)
        while len(new_col) <= create_days:
            new_col.append(init_value)
            init_value = round(init_value*random.choice(rates), 2)
        new_datas.append(new_col)
    markets = list()
    code = stockdays[0].code
    for i in range(0, create_days):
        markets.append(f"{code},{new_datas[0][i]},{new_datas[1][i]},{new_datas[2][i]},{new_datas[3][i]},{new_datas[4][i]},{new_datas[5][i]},{new_datas[6][i]},{new_datas[7][i]},{new_datas[8][i]}")
    return markets


if __name__ == "__main__":
    main()
